﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TracedReality
{
    /// <summary>
    /// An arbitrary utility class. These are apparently not good in software design. Oh well.
    /// </summary>
    public class Utilities
    {
        private static readonly double srgb_alpha = 0.055;

        /// <summary>
        /// Convert a linear value in the approximate range [0,1] to sRGB.
        /// </summary>
        /// <param name="l">Linear value to convert.</param>
        /// <returns>The sRGB value corresponding to the given linear value.</returns>
        public static double linearToSRGB(double l)
        {
            if (l < 0.0031308) { return 12.92 * l; }

            return (1 + srgb_alpha) * (Math.Pow(l, 1 / 2.4) - srgb_alpha);
        }

        /// <summary>
        /// Convert a sRGB value in the approximate range [0,1] to linear.
        /// </summary>
        /// <param name="l">sRGB value to convert.</param>
        /// <returns>The linear value corresponding to the given sRGB value.</returns>
        public static double srgbToLinear(double s)
        {
            if (s <= 0.04045) { return s / 12.92; }

            return Math.Pow((s + srgb_alpha) / (1 + srgb_alpha), 2.4);
        }

        /// <summary>
        /// Check if the given incoming direction, normal, and indices of refraction
        /// indicate total internal reflection.
        /// </summary>
        /// <param name="i">The incoming direction.</param>
        /// <param name="n">The normal.</param>
        /// <param name="eta1">The index of refraction of the "outside" material.</param>
        /// <param name="eta2">The index of refraction of the "inside" material</param>
        /// <returns>True if total internal reflection should occur, false otherwise.</returns>
        public static bool totalInternalRefraction(Vector i, Vector n, double eta1, double eta2)
        {
            if (eta1 > eta2)
            {
                double eta = eta1 / eta2;
                double costhi = i.dot(-n);
                return (eta * eta) * (1 - (costhi * costhi)) > 1;
            }

            return false;
        }

        /// <summary>
        /// Computes the amount of light that should be reflected based on the given
        /// incoming direction, normal, and indices of refraction.
        /// </summary>
        /// <param name="incoming">The incoming direction.</param>
        /// <param name="normal">The normal.</param>
        /// <param name="eta1">The index of refraction of the "outside" material.</param>
        /// <param name="eta2">The index of refraction of the "inside" material</param>
        /// <returns>The reflectance with the given parameters.</returns>
        public static double computeReflectance(Vector incoming, Vector normal, double eta1, double eta2)
        {
            if (totalInternalRefraction(incoming, normal, eta1, eta2))
            {
                return 1;
            }
            else
            {
                double costhi = incoming.dot(-normal);
                double eta = eta1 / eta2;
                double rad = 1 - (eta * eta) * (1 - (costhi * costhi));
                double costht = Math.Sqrt(rad);
                double Rpe = ((eta1 * costhi) - (eta2 * costht)) / ((eta1 * costhi) + (eta2 * costht));
                double Rpa = ((eta2 * costhi) - (eta1 * costht)) / ((eta2 * costhi) + (eta1 * costht));

                return (Rpe*Rpe + Rpa*Rpa) / 2.0;
            }
        }

        /// <summary>
        /// Stratify the given plane with the given number of samples on each side.
        /// May also specify whether or not to randomize the point inside the strata.
        /// </summary>
        /// <param name="p">Corner of the plane.</param>
        /// <param name="d1">The first direction of the plane.</param>
        /// <param name="d2">The second direction of the plane.</param>
        /// <param name="samples">How many samples per side.</param>
        /// <param name="randomize">Whether or not to randomize the resulting point.</param>
        /// <param name="r">Random to use.</param>
        /// <returns>A two-dimension array of Vectors containing either the center point or a random point
        /// of the resulting strata.</returns>
        public static Vector[,] stratify(Vector p, Vector d1, Vector d2, int samples, bool randomize, Random r)
        {
            Vector[,] result = new Vector[samples, samples];
            Vector dd1 = d1 / (double)samples;
            Vector dd2 = d2 / (double)samples;

            for (int a = 0; a < samples; a++)
            {
                for (int b = 0; b < samples; b++)
                {
                    double x = a + (randomize ? r.NextDouble() : 0.5);
                    double y = b + (randomize ? r.NextDouble() : 0.5);
                    result[a, b] = p + (x * dd1) + (y * dd2);
                }
            }

            return result;
        }

        /// <summary>
        /// Tile the given plane with the given number of samples on each side.
        /// </summary>
        /// <param name="p">The corner of the plane.</param>
        /// <param name="d1">The first direction of the plane.</param>
        /// <param name="d2">The second direction of the plane.</param>
        /// <param name="samples">How many samples per side.</param>
        /// <returns>A two-dimensional array of Vectors containing the corners of the resulting tiles.</returns>
        public static Vector[,] tile(Vector p, Vector d1, Vector d2, int samples)
        {
            Vector[,] result = new Vector[samples, samples];
            Vector dd1 = d1 / (double)samples;
            Vector dd2 = d2 / (double)samples;

            for (int a = 0; a < samples; a++)
            {
                for (int b = 0; b < samples; b++)
                {
                    result[a, b] = p + (a * dd1) + (b * dd2);
                }
            }

            return result;
        }

        public class Tile
        {
            public int x;
            public int y;
            public int width;
            public int height;
        }

        public static Tile[] tile(int width, int height, int samples)
        {
            Tile[] result = new Tile[samples * samples];
            double twidth = width / (double)samples;
            double theight = height / (double)samples;
            int curx = 0;
            int cury = 0;

            for (int x = 0; x < samples; x++)
            {
                cury = 0;
                int xoff = (int)Math.Ceiling((x * twidth) - curx);

                for (int y = 0; y < samples; y++)
                {
                    int yoff = (int)Math.Ceiling((y * theight) - cury);

                    result[(x * samples) + y] = new Tile
                    {
                        x = curx,
                        y = cury,
                        width = (int)Math.Floor(twidth) + xoff,
                        height = (int)Math.Floor(theight) + yoff
                    };

                    cury += (int)Math.Floor(theight) + yoff;
                }

                curx += (int)Math.Floor(twidth) + xoff;
            }

            return result;
        }

        /// <summary>
        /// Flatten a given generic two-dimensional array into a one-dimensional array.
        /// </summary>
        /// <typeparam name="T">Type parameter.</typeparam>
        /// <param name="arr">The array to flatten.</param>
        /// <returns>A flattened version of the given array.</returns>
        public static T[] flatten<T>(T[,] arr)
        {
            T[] res = new T[arr.Length];
            int a = 0;
            foreach (T el in arr)
            {
                res[a] = el;
                a++;
            }

            return res;
        }

        /// <summary>
        /// Uniformly sample a given population.
        /// </summary>
        /// <typeparam name="T">Type parameter</typeparam>
        /// <param name="arr">The population to sample</param>
        /// <param name="samples">How many samples to take.</param>
        /// <param name="r">Random to use.</param>
        /// <returns>An array containing a uniformly random sample of the given population.</returns>
        public static T[] sample<T>(T[] arr, int samples, Random r)
        {
            // Thanks to
            // http://stackoverflow.com/questions/5416567/random-selection
            if (samples > arr.Length) { return arr; }
            
            T[] res = new T[samples];
            int a = 0;

            for (int b = 0; b < arr.Length; b++)
            {
                if (a >= samples) { break; }
                double prob = (samples - a) / (double)(arr.Length - b);

                if (r.NextDouble() < prob)
                {
                    res[a] = arr[b];
                    a++;
                }
            }

            return res;
        }
    }
}
